Amazon S3의 “폴더”라는 환상을 파괴하고 그 실체를 알아보기
안녕하세요 DA사업본부 송영진입니다.
지난 번에 Amazon S3에서 폴더 이름 바꾸는 방법이 없나요? 라는 제목의 블로그를 작성 했었는데요, 오늘은 그 이해를 돕기 위해서 매우 도움이 되었던 都元ダイスケ님의 블로그를 번역한 내용으로 전달해보려고 합니다.
인삿말
잘 훈련된 애플 추종자, 미야모토입니다. Amazon S3 에 대한 자세한 설명은 필요하지 않겠지만, 요컨대 파일 스토리지입니다. HTTP 기반으로 파일을 업로드할 수 있고 다운로드할 수 있는 서비스입니다.
예전부터 데이터는 직렬화 된 형식으로 파일이라는 단위로 저장되고 관리되어 왔습니다. 로컬 머신내에서 파일을 관리하는 구조가 파일 시스템으로, 그 대부분에는 폴더라고 하는 계층 구조를 취급하는 구조가 갖추어져 있습니다.
Amazon S3는 Management Console을 통해 폴더를 만들고 그 안에 추가 폴더를 만들거나 파일을 저장할 수 있습니다. 그러나.
Amazon S3에는 사실 폴더라는 개념은 없다
입니다.
Amazon S3의 기본 기술은 단순한 KVS(Key-Value 유형 데이터스토어)일 뿐입니다. 예를 들면 아래와 같은 폴더(라고 우리가 인식하고 있는) 구조가 있다고 합니다. (본 엔트리에서는, bar.txt에는 bar , baz.txt에는 baz 라는 텍스트가 들어 있다고 간단하게 생각하기로 합니다.)
(루트) └ foo/ └ bar.txt
하지만 위의 내용은 우리가 이렇게 인식하고 있을 뿐이지, S3내부에서는 단순히 아래와 같은 정보를 보관 유지하고 있는 것에 지나지 않습니다. S3에서는 기본적으로 / 에 특별한 의미는 없습니다.
Key (전체 경로) | Value (파일 내용) |
---|---|
foo/bar.txt | bar |
Amazon S3에서 비어있는 폴더의 표현
여기서, S3가 이러한 구조로 정보를 보유하고 있다면 다음에 신경쓰이는 것은 비어있는 폴더를 어떻게 처리하느냐 입니다. 파일의 실체가 없으면 빈 폴더를 표현할 수 없습니다.
관리 콘솔에서 비어있는 폴더 만들기
예를 들어 관리 콘솔에서 새로 만든 버킷의 루트 바로 아래에 foo라는 빈 폴더를 만들 때 S3에서 파일 크기 0의 foo/라는 요소를 만듭니다 .
Key (전체 경로) | Value (파일 내용) |
---|---|
foo/ | (비어있음) |
$ aws s3api list-objects --bucket cm-song-test { "Contents": [ { "Key": "foo/", "LastModified": "2023-09-21T09:14:11+00:00", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "Size": 0, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } } ] }
위와 같이 오브젝트는 있다고 나오지만 사이즈 0으로 됩니다.
관리 콘솔에서 기존 빈 폴더 안에 파일을 배치(케이스 1)
위의 빈 폴더에 bar.txt를 배치하면 다음과 같이 결과값이 변합니다.
Key (전체 경로) | Value (파일 내용) |
---|---|
foo/ | (비어있음) |
foo/bar.txt | bar |
$ aws s3api list-objects --bucket cm-song-test { "Contents": [ { "Key": "foo/", "LastModified": "2023-09-21T09:14:11+00:00", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "Size": 0, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } }, { "Key": "foo/bar.txt", "LastModified": "2023-09-21T09:20:38+00:00", "ETag": "\"c157a79031e1c40f85931829bc5fc552\"", "Size": 4, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } } ] }
관리 콘솔로에서 보면 파일은 1개 밖에 없지만, 실제 S3 위에서는 이러한 2가지의 오브젝트가 관리되고 있습니다.
aws-cli를 사용하여 존재하지 않는 폴더에 파일을 직접 배치(케이스 2)
그렇다면 새로 만든 방금 버킷의 루트 디렉토리 바로 아래에 aws-cli를 사용하여 아래와 같은 명령으로 파일을 배치하면 어떻게 될까요?
$ echo bar > bar.txt $ aws s3api put-object --bucket cm-song-test --key "foo/bar.txt" --body bar.txt
결과는 다음과 같습니다.
Key (전체 경로) | Value (파일 내용) |
---|---|
foo/bar.txt | bar |
$ aws s3api list-objects --bucket cm-song-test { "Contents": [ { "Key": "foo/bar.txt", "LastModified": "2023-09-21T09:31:11+00:00", "ETag": "\"c157a79031e1c40f85931829bc5fc552\"", "Size": 4, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } } ] }
oh... 케이스 1과 2 사이에, 우리의 머리 속에서는 큰 차이는 없습니다만, S3적으로는 조금 다른 상황이 되고 있네요. 뭐, 그렇게 큰 문제가 되지 않는다고 생각합니다만.
덤으로 케이스 2의 상태에서 관리 콘솔에서 bar.txt를 삭제하면 foo/ 폴더가 새롭게 만들어집니다.
오브젝트의 리스트업
자, 여기서부터는 조금 복잡한 환경을 가정해두겠습니다. 이런 식으로.
(루트) ├ foo/ │ ├ bar/ │ │ └ qux.txt │ ├ baz/ │ ├ quux/ │ │ ├ corge.txt │ │ └ grault.txt │ └ garply.txt └ waldo.txt
S3에서는, 이렇게 표현할 수 있습니다. (일부러 폴더를 나타내는 빈 파일이 있거나 없거나 하는 상황을 만들어내고 있습니다.)
Key (전체 경로) | Value (파일 내용) |
---|---|
foo/ | (비어있음) |
foo/bar/qux.txt | qux |
foo/baz/ | (비어있음) |
foo/quux/ | (비어있음) |
foo/quux/corge.txt | corge |
foo/quux/grault.txt | grault |
foo/garply.txt | garply |
waldo.txt | waldo |
모든 오브젝트 리스트업
이 상태에 대해, 모든 파일을 일람하는 명령어(API 액션)와 파라미터는, 지금까지 한 것과 같이, 이런 느낌입니다. (출력을 단순화하기 위해 jq 로 키만 출력하고 있습니다.)
$ aws s3api list-objects --bucket cm-song-test | jq ".Contents[].Key" "foo/" "foo/bar/qux.txt" "foo/baz/" "foo/garply.txt" "foo/quux/" "foo/quux/corge.txt" "foo/quux/grault.txt" "waldo.txt"
8건의 결과가 표시되었습니다.
prefix를 지정한 필터링
그런 다음 foo/ 폴더의 파일만 나열하려면 prefix
라는 옵션(매개 변수)을 부여합니다.
$ aws s3api list-objects --bucket cm-song-test --prefix "foo/" | jq ".Contents[].Key" "foo/" "foo/bar/qux.txt" "foo/baz/" "foo/garply.txt" "foo/quux/" "foo/quux/corge.txt" "foo/quux/grault.txt"
이와 같이, prefix로 지정했을 경우, 그 문자열로 시작되는 키를 가지는 오브젝트 7건이 표시됩니다. 딱히 prefix 지정을 / 로 끝낼 필요는 없고, 이런 지정도 가능합니다.
$ aws s3api list-objects --bucket cm-song-test --prefix "foo/b" | jq ".Contents[].Key" "foo/bar/qux.txt" "foo/baz/"
특정 폴더 내부의 오브젝트만 표시하기 위해서는
파일 시스템에서 위와 같은 구조로 이루어져 있을 때, 현재 디렉토리가 foo인 경우, 그 때 리스트업하고 싶은 파일들은 "그 폴더 내부의 파일들만"이 일반적일 것입니다. 위와 같이 prefix로 좁히는 것만으로는, 예를 들어 foo/bar/ 폴더 안에 파일이 10000개 들어있었을 경우, 뒤쪽의 처리는 조금 번거로울 것 같고, 데이터의 취급으로서도 비효율적입니다.
여기서 list-objects의 결과에 몰래 존재하는 CommonPrefixes 라는 출력을 사용해서 해결 할 수 있습니다. 방금 전 7개의 결과가 표시된 명령에 --delimiter "/"
옵션을 추가하여 실행해 봅니다.
$ aws s3api list-objects --bucket cm-song-test --prefix "foo/" --delimiter "/" { "Contents": [ { "Key": "foo/", "LastModified": "2023-09-21T13:29:46+00:00", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "Size": 0, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } }, { "Key": "foo/garply.txt", "LastModified": "2023-09-21T13:32:15+00:00", "ETag": "\"cda2d9c3869a5fa5c4e09340afa8062b\"", "Size": 7, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } } ], "CommonPrefixes": [ { "Prefix": "foo/bar/" }, { "Prefix": "foo/baz/" }, { "Prefix": "foo/quux/" } ] }
무려 Contents에 출력된 것은, foo/ 폴더 그 자체와, 그 바로 내부에 있는 garply.txt에만 한정되었습니다. 그리고 CommonPrefixes로서, foo 바로 아래에 있는 3개의 폴더 bar, baz, quux가 나열되어 있습니다.
이와 같이 list-objects 액션에 대해 delimiter 파라미터로 경로의 단락 문자를 지정하면, 각 요소에 대해서 prefix 뒤에 최초로 나타나는 delimiter까지의 문자열로 그룹을 지어서………. 말로는 조금 설명하기 어려운데, 어쨌든 이렇게 "폴더로 인식 할 수 있는 것의 목록"을 CommonPrefixes에 출력할 수 있는 것 입니다.
Amazon S3 관리 콘솔
위의 내용처럼 S3는 단순한 Key-Value형 스토리지일 뿐입니다만, list-object 액션에 prefix
나 delimiter
라는 파라미터를 추가함으로써, 특정의 폴더 내부의 파일들만을 나열하는 것 같은 행동을 실현할 수 있게 되어 있습니다. / 를 경로 구분 문자로 특별히 취급하는 것은 Amazon S3가 아니라 그 위에 있는 "관리 콘솔"입니다.
Amazon S3 관리 콘솔은 이러한 메커니즘을 이용하여 순수한 S3로서는 개념이 존재하지 않는 '폴더'라는 환상을 우리에게 보여주고 있었습니다.
덤으로
전혀 실용성은 없습니다만, 아래와 같은 커멘드 결과에 대해서, 왜 이런 출력이 되었는지, 각자 고찰해 봐 주세요.
이 덤을 즐길 수 있는 분은, 이쪽 으로 부디.
$ aws s3api list-objects --bucket cm-song-test --prefix "fo" --delimiter "q" { "Contents": [ { "Key": "foo/", "LastModified": "2023-09-21T13:29:46+00:00", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "Size": 0, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } }, { "Key": "foo/baz/", "LastModified": "2023-09-21T13:30:16+00:00", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "Size": 0, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } }, { "Key": "foo/garply.txt", "LastModified": "2023-09-21T13:32:15+00:00", "ETag": "\"cda2d9c3869a5fa5c4e09340afa8062b\"", "Size": 7, "StorageClass": "STANDARD", "Owner": { "DisplayName": "xxxxxxxxxxxx", "ID": "xxxxxxxxxxxxxxxxxxxxxxxx" } } ], "CommonPrefixes": [ { "Prefix": "foo/bar/q" }, { "Prefix": "foo/q" } ] }
마지막으로
어떠셨나요, S3는 진짜 파일 시스템이 아니라 Key-Value 스토리지라는 말 이해가 되셨나요? 저도 이 블로그를 읽고나서야 왜 어떤 때는 S3에서 리스트 가져올 때 폴더가 나오고 어떤 때는 안나오는지 이해가 가능했습니다. 여러분들께도 S3의 폴더에 대한 환상을 깨게 되는 계기가 되길 바라면서 이만 마치겠습니다. 감사합니다!
PS. 덤의 힌트는 0바이트인 폴더의 유무입니다.